iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Software Development

Polars熊霸天下系列 第 17

[Day17] - 三種join

  • 分享至 

  • xImage
  •  

今天我們來說明Polars提供的三種join,分別為「"equi joins"」、「"non-equi joins"」、及「"asof join"」。

本日大綱如下:

  1. 本日引入模組及準備工作
  2. Equi joins
  3. Non-equi joins
  4. Asof join
  5. codepanda

0. 本日引入模組及準備工作

from datetime import date

import polars as pl

df1 = pl.DataFrame(
    {"name": ["Tom", "Lisa", "John"], "has_pet": ["Y", "N", "Y"]}
)
shape: (3, 2)
┌──────┬─────────┐
│ name ┆ has_pet │
│ ---  ┆ ---     │
│ str  ┆ str     │
╞══════╪═════════╡
│ Tom  ┆ Y       │
│ Lisa ┆ N       │
│ John ┆ Y       │
└──────┴─────────┘
df2 = pl.DataFrame(
    {
        "name": ["Lisa", "John", "Vincent", "Mary"],
        "lucky_number": [25, 36, 7, 2],
    }
)
shape: (4, 2)
┌─────────┬──────────────┐
│ name    ┆ lucky_number │
│ ---     ┆ ---          │
│ str     ┆ i64          │
╞═════════╪══════════════╡
│ Lisa    ┆ 25           │
│ John    ┆ 36           │
│ Vincent ┆ 7            │
│ Mary    ┆ 2            │
└─────────┴──────────────┘

1. Equi joins

「"Equi joins"」的意思是根據兩個dataframe中所指定的相同列名進行合併。

在Polars中,我們使用pl.DataFrame.join()來進行「"equi joins"」。例如,藉由設定on=為針對df1df2的「"name"」列進行合併:

df1.join(df2, on="name")
# equivalent to 
# df1.join(df2, on="name", how="inner")
shape: (2, 3)
┌──────┬─────────┬──────────────┐
│ name ┆ has_pet ┆ lucky_number │
│ ---  ┆ ---     ┆ ---          │
│ str  ┆ str     ┆ i64          │
╞══════╪═════════╪══════════════╡
│ Lisa ┆ N       ┆ 25           │
│ John ┆ Y       ┆ 36           │
└──────┴─────────┴──────────────┘

可以觀察到合併結果為兩行,這是因為「"Lisa"」跟「"John"」是兩個dataframe的「"name"」列都具有的行。這種合併的方式稱為「"inner"」,是how=參數的預設值。

how=參數一共有八種,以下我們再舉「"left"」、「"semi"」及兩個為例子。

how=設定為「"left"」時:

df1.join(df2, on="name", how="left")
shape: (3, 3)
┌──────┬─────────┬──────────────┐
│ name ┆ has_pet ┆ lucky_number │
│ ---  ┆ ---     ┆ ---          │
│ str  ┆ str     ┆ i64          │
╞══════╪═════════╪══════════════╡
│ Tom  ┆ Y       ┆ null         │
│ Lisa ┆ N       ┆ 25           │
│ John ┆ Y       ┆ 36           │
└──────┴─────────┴──────────────┘

可以觀察到合併結果為三行,且三行都是df1所擁有的。請留意,因為在df2中沒有「"Tom"」,所以Polars在「"lucky_number"」列將該值填入null。

how=設定為「"semi"」時:

df1.join(df2, on="name", how="semi")
shape: (2, 2)
┌──────┬─────────┐
│ name ┆ has_pet │
│ ---  ┆ ---     │
│ str  ┆ str     │
╞══════╪═════════╡
│ Lisa ┆ N       │
│ John ┆ Y       │
└──────┴─────────┘

可以觀察到合併結果和使用「"inner"」時一樣,皆為兩行,但卻只剩下兩列。這是因為「"semi"」的效果更像是一個過濾器,會依據「"name"」列,找出兩個dataframe共同的行(即「"Lisa"」及「"John"」兩行),但卻不將df2中的列加入結果之中(即df2的「"lucky_number"」列)。

最後,我們介紹left_on=right_on=,可以讓我們合併兩個不同列名,而且這些列內的值甚至可以使用expr表示。舉例來說,先建立一個df3如下:

df3 = df2.select(
    pl.col("name").str.to_lowercase().alias("uname"), "lucky_number"
)
shape: (4, 2)
┌─────────┬──────────────┐
│ uname   ┆ lucky_number │
│ ---     ┆ ---          │
│ str     ┆ i64          │
╞═════════╪══════════════╡
│ lisa    ┆ 25           │
│ john    ┆ 36           │
│ vincent ┆ 7            │
│ mary    ┆ 2            │
└─────────┴──────────────┘

接下來,將df1的「"name"」列與以df3「"uname"」列為基礎所建構之expr合併:

(
    df1.join(
        df3, left_on="name", right_on=pl.col("uname").str.to_titlecase()
    )
)
shape: (2, 4)
┌──────┬─────────┬───────┬──────────────┐
│ name ┆ has_pet ┆ uname ┆ lucky_number │
│ ---  ┆ ---     ┆ ---   ┆ ---          │
│ str  ┆ str     ┆ str   ┆ i64          │
╞══════╪═════════╪═══════╪══════════════╡
│ Lisa ┆ N       ┆ lisa  ┆ 25           │
│ John ┆ Y       ┆ john  ┆ 36           │
└──────┴─────────┴───────┴──────────────┘

當然您也可以反過來寫,將以df1「"name"」列為基礎所建構之expr與df3的「"uname"」列合併:

(
    df1.join(
        df3, left_on=pl.col("name").str.to_lowercase(), right_on="uname"
    )
)
shape: (2, 4)
┌──────┬─────────┬───────┬──────────────┐
│ name ┆ has_pet ┆ uname ┆ lucky_number │
│ ---  ┆ ---     ┆ ---   ┆ ---          │
│ str  ┆ str     ┆ str   ┆ i64          │
╞══════╪═════════╪═══════╪══════════════╡
│ Lisa ┆ N       ┆ lisa  ┆ 25           │
│ John ┆ Y       ┆ john  ┆ 36           │
└──────┴─────────┴───────┴──────────────┘

兩者結果相同。

2. Non-equi joins

「"Non-equi joins"」的意思是根據給定的單或多個以expr建構的判斷條件,來合併兩個dataframe。

在Polars中,我們使用pl.DataFrame.join_where()來進行「"non-equi joins"」。舉例來說,先建立一個df4如下:

df4 = pl.DataFrame({"name": ["Caroline", "Bob"], "lucky_number2": [3, 40]})
shape: (2, 2)
┌──────────┬───────────────┐
│ name     ┆ lucky_number2 │
│ ---      ┆ ---           │
│ str      ┆ i64           │
╞══════════╪═══════════════╡
│ Caroline ┆ 3             │
│ Bob      ┆ 40            │
└──────────┴───────────────┘

接下來,將df2df4進行「"non-equi joins"」,合併條件為df2的「"lucky_number"」列是否小於df4的「"lucky_number2"」列:

(
    df2.join_where(df4, pl.col("lucky_number").lt(pl.col("lucky_number2")))
)
shape: (5, 4)
┌─────────┬──────────────┬────────────┬───────────────┐
│ name    ┆ lucky_number ┆ name_right ┆ lucky_number2 │
│ ---     ┆ ---          ┆ ---        ┆ ---           │
│ str     ┆ i64          ┆ str        ┆ i64           │
╞═════════╪══════════════╪════════════╪═══════════════╡
│ Mary    ┆ 2            ┆ Caroline   ┆ 3             │
│ Mary    ┆ 2            ┆ Bob        ┆ 40            │
│ Vincent ┆ 7            ┆ Bob        ┆ 40            │
│ Lisa    ┆ 25           ┆ Bob        ┆ 40            │
│ John    ┆ 36           ┆ Bob        ┆ 40            │
└─────────┴──────────────┴────────────┴───────────────┘

其結果有五行,且我們看到了有兩行皆含有「"Mary"」,究竟這是如何計算出來的呢?

原來Polars會將df2中每一行的「"lucky_number"」列值與df4每一行的「"lucky_number2"」列值進行比較,總共會進行兩輪共4x2=8次比較,最後會留下結果為True的行。

第一輪中,由df2每一行的「"lucky_number"」列值與df4第一行,即「"Caroline"」的「"lucky_number2"」列值3進行比較。結果,只有df2「"Mary"」的「"lucky_number"」列值2小於3,所以僅有一行符合條件。

而第二輪比較中,由df2每一行的「"lucky_number"」列值與df4第二行,即「"Bob"」的「"lucky_number2"」列值40進行比較。結果,df2每一行的「"lucky_number"」列值皆小於40,所以共有四行符合條件。

接著,讓我們觀察將df4df2進行「"non-equi joins"」的結果:

(
    df4.join_where(df2, pl.col("lucky_number").lt(pl.col("lucky_number2")))
)
shape: (5, 4)
┌──────────┬───────────────┬────────────┬──────────────┐
│ name     ┆ lucky_number2 ┆ name_right ┆ lucky_number │
│ ---      ┆ ---           ┆ ---        ┆ ---          │
│ str      ┆ i64           ┆ str        ┆ i64          │
╞══════════╪═══════════════╪════════════╪══════════════╡
│ Bob      ┆ 40            ┆ John       ┆ 36           │
│ Bob      ┆ 40            ┆ Lisa       ┆ 25           │
│ Bob      ┆ 40            ┆ Vincent    ┆ 7            │
│ Bob      ┆ 40            ┆ Mary       ┆ 2            │
│ Caroline ┆ 3             ┆ Mary       ┆ 2            │
└──────────┴───────────────┴────────────┴──────────────┘

可以發現其結果是相同的,但是由於這次是以df4df2進行「"non-equi joins"」,所以「"name"」列之值會來自df4,而「"name_right"」列之值會來自df2

最後,如果您不喜歡預設後綴「"_right"」,可以使用suffix=參數來指定想要的後綴。

3. Asof join

「"Asof join"」是一種特殊的合併方式,是基於兩個dataframe中所指定列名的相近程度來合併。

在Polars中,我們使用pl.DataFrame.join_asof()來進行「"asof join"」。

其中提到:

This is similar to a left-join except that we match on nearest key rather than equal keys.

最常作為on=參數的型別為temporal型別。舉例來說,現在有套件A及套件B各自的發佈日期及版本如下:

package_a = pl.DataFrame(
    {
        "date": [
            date(2025, 5, 14),
            date(2025, 6, 16),
            date(2025, 7, 29),
            date(2025, 8, 16),
            date(2025, 9, 2),
        ],
        "version": ["v1.1.2", "v1.2.1", "v1.2.2", "v1.3.1", "v1.4.5"],
    }
)
shape: (5, 2)
┌────────────┬─────────┐
│ date       ┆ version │
│ ---        ┆ ---     │
│ date       ┆ str     │
╞════════════╪═════════╡
│ 2025-05-14 ┆ v1.1.2  │
│ 2025-06-16 ┆ v1.2.1  │
│ 2025-07-29 ┆ v1.2.2  │
│ 2025-08-16 ┆ v1.3.1  │
│ 2025-09-02 ┆ v1.4.5  │
└────────────┴─────────┘
package_b = pl.DataFrame(
    {
        "date": [date(2025, 6, 1), date(2025, 7, 5), date(2025, 8, 19)],
        "version": ["v2.0.1", "v2.0.2", "v2.0.3"],
    }
)
shape: (3, 2)
┌────────────┬─────────┐
│ date       ┆ version │
│ ---        ┆ ---     │
│ date       ┆ str     │
╞════════════╪═════════╡
│ 2025-06-01 ┆ v2.0.1  │
│ 2025-07-05 ┆ v2.0.2  │
│ 2025-08-19 ┆ v2.0.3  │
└────────────┴─────────┘

我們可以使用pl.DataFrame.join_asof()來找出當套件B發佈時,最新的套件A版本為何:

package_b.join_asof(package_a, on="date", strategy="backward")
shape: (3, 3)
┌────────────┬─────────┬───────────────┐
│ date       ┆ version ┆ version_right │
│ ---        ┆ ---     ┆ ---           │
│ date       ┆ str     ┆ str           │
╞════════════╪═════════╪═══════════════╡
│ 2025-06-01 ┆ v2.0.1  ┆ v1.1.2        │
│ 2025-07-05 ┆ v2.0.2  ┆ v1.2.1        │
│ 2025-08-19 ┆ v2.0.3  ┆ v1.3.1        │
└────────────┴─────────┴───────────────┘

如果設定coalesce=False,則可以同時顯示來自兩個dataframe的日期及版本資訊:

(
    package_b.join_asof(
        package_a, on="date", strategy="backward", coalesce=False
    )
)
shape: (3, 4)
┌────────────┬─────────┬────────────┬───────────────┐
│ date       ┆ version ┆ date_right ┆ version_right │
│ ---        ┆ ---     ┆ ---        ┆ ---           │
│ date       ┆ str     ┆ date       ┆ str           │
╞════════════╪═════════╪════════════╪═══════════════╡
│ 2025-06-01 ┆ v2.0.1  ┆ 2025-05-14 ┆ v1.1.2        │
│ 2025-07-05 ┆ v2.0.2  ┆ 2025-06-16 ┆ v1.2.1        │
│ 2025-08-19 ┆ v2.0.3  ┆ 2025-08-16 ┆ v1.3.1        │
└────────────┴─────────┴────────────┴───────────────┘

另一個常見的應用則為股票的下單與成交,有興趣的朋友可以參考教學文件

4. codepanda

Pandas的join主要有pd.DataFrame.merge()pd.DataFrame.join()兩種。pd.DataFrame.merge()較為通用,而pd.DataFrame.join()雖然較為挶限,但可作為想要快速對齊索引的便捷函數。

此外,Pandas的asof join可以參考pd.merge_asof()函數。

Code

本日程式碼傳送門


上一篇
[Day16] - pl.DataFrame.pivot()、pl.DataFrame.unpivot()及pl.DataFrame.unstack()
下一篇
[Day18] - 進階操作分享
系列文
Polars熊霸天下29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言